﻿// Nathan Moschkin's universal scalable control template for .NET Framework 4.0+.
// Scalable scrolling container control with custom paint

using System;
using System.IO;
using System.Threading;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.ComponentModel;

namespace CtlTplCS { 

    public partial class CSUCTemplate : UserControl
    {
                
    #region Public Events

    /// <summary>
    /// Custom event handler arguments for the user-drawn control template.
    /// </summary>
    public class OControlEventArgs : EventArgs
    {
        
    #region Private Variables
        
        private bool canCancel = false, 
                        cancel = false, 
                        cancelled = false;

        private System.Drawing.Point location;
        private System.Drawing.Size size;
        private Image newImage = null, 
                      oldImage = null;
        
        private Object oldValue = null;
        private Object newValue = null;

    #endregion

    #region Read-Only Properties

        public Rectangle Bounds
        {
            get {
                return new Rectangle(location, size);
            }
        }

        public Object OldValue
        {
            get {
                return oldValue;
            }
        }

        public Object NewValue {
            get {
                return newValue;
            }
        }

        public System.Drawing.Image OldImage
        {
            get {
                return oldImage;
            }
        }

        public System.Drawing.Image NewImage
        {
            get {
                return newImage;
            }
        }

        public bool CanCancel 
        {
            get {
                return canCancel;
            }
        }

        public bool Cancelled 
        {
            get {
                return cancelled;
            }
        }

    #endregion

    #region Internal-Set Properties

        public Size Size 
        {
            get {
                return size;
            }
            internal set 
            {
                size = value;
            }
        }

        public System.Drawing.Point Location
        {
            get {
                return location;
            }
            internal  set {
                location = value;
            }
        }

    #endregion

    #region Public Properties

        public bool Cancel 
        {
            get {
                return cancel;
            }
            set {

                if (canCancel == false)
                {
                    throw new ArgumentException("Property cannot be set for this instance.", "Cancel");
                }

                cancel = value;
                if (value == true) cancelled = true;
            }
        }

    #endregion

    #region Constructors

        internal  OControlEventArgs(Rectangle rect, bool canCancel)
        {
            location = new System.Drawing.Point(rect.X, rect.Y);
            size = new System.Drawing.Size(rect.Width, rect.Height);
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(System.Drawing.Point loc, System.Drawing.Size size, bool canCancel = false)
        {
            location = new System.Drawing.Point(loc.X, loc.Y);
            size = new System.Drawing.Size(size.Width, size.Height);
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(System.Drawing.Size size, bool canCancel = false)
        {
            size = new System.Drawing.Size(size.Width, size.Height);
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(System.Drawing.Point loc, bool canCancel = false)
        {
            Location = new System.Drawing.Point(loc.X, loc.Y);
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(System.Drawing.Image oldImage, System.Drawing.Image newImage, bool canCancel = false)
        {
            this.oldImage = oldImage;
            this.newImage = newImage;
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(double oldValue, double newValue, bool canCancel = false)
        {
            this.oldValue = oldValue;
            this.newValue = newValue;
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(String oldValue, String newValue, bool canCancel = false)
        {
            this.oldValue = oldValue;
            this.newValue = newValue;
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(int oldValue, int newValue, bool canCancel = false)
        {
            this.oldValue = oldValue;
            this.newValue = newValue;
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(Object oldValue, Object newValue, bool canCancel = false)
        {
            this.oldValue = oldValue;
            this.newValue = newValue;
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(bool canCancel, bool didCancel)
        {
            cancelled = didCancel;
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs(bool canCancel)
        {
            this.canCancel = canCancel;
        }

        internal  OControlEventArgs()
        {

        }

    #endregion

    }

    public delegate void UCDocEventHandler(Object sender, OControlEventArgs e);

    public event UCDocEventHandler ImageChanging;
    public event UCDocEventHandler ImageChanged;
    public event UCDocEventHandler CanvasRedrawn; 
    public event UCDocEventHandler ScaleChanging;
    public event UCDocEventHandler ScaleChanged;
    //public event UCDocEventHandler SelectionChanging;
    //public event UCDocEventHandler SelectionChanged;

    #endregion

    #region Private Non-Property Variables
        private float mdpix, mdpiy;
        private float mdpmx, mdpmy;
        
        private float lastScale = 1.0F;
        private System.Drawing.Image paintPic = null;
        private bool needRedraw = true;

    #endregion

    #region Private Property Variables

        /* private property variables */
        private System.Drawing.Rectangle imageRect;
        private System.Drawing.Image image = null;

        private int maxScrollX = 0;
        private int maxScrollY = 0;
    
        private double scaling = 1.0;
        private String fileName = "";
        
    #endregion

    #region Public Properties

    [Browsable(false)]
    public Rectangle ImageBounds 
    {
        get {
            return imageRect;
        }
    }

    [Browsable(true), Description("The scale factor for the image and all the controls."), Category("Appearance")]
    public double Scaling 
    {
        get 
        {
            return scaling;
        }
        set 
        {

            OControlEventArgs e;
            double ov = scaling;

                
            if (value < 0.2) value = 0.2;
            if (value > 10.0) value = 10.0;

            value = System.Math.Round(value, 4);

            e = new OControlEventArgs(ov, value, true);
            if (ScaleChanging != null) {
                ScaleChanging(this, e);
            }

            if (e.Cancelled) return;

            if (value == scaling) return;

            ScaleControls(true);

            Scaling = value;
            needRedraw = true;

            CalculateDimensions();

            this.Invalidate();

            e = new OControlEventArgs(ov, value, false);
            if (ScaleChanged != null) {
                ScaleChanged(this, e);
            }

        }
    }

    [Browsable(true), Description("The image rectangle."), Category("Behavior")]
    public Rectangle ImageRect
    {
        get
        {
            return imageRect;
        }
        internal set
        {
            imageRect = value;
        }
    }

    [Browsable(true), Description("The image to be loaded (or saved) in the background"), Category("Behavior")]
    public System.Drawing.Image Image
    {
        get 
        {
            return image;
        }
        set 
        {
            System.Drawing.Image oi = image;

            System.Drawing.Point ptO = this.AutoScrollOffset;

            OControlEventArgs e = new OControlEventArgs(oi, value, true);

            if (ImageChanging != null)
                ImageChanging(this, e);
            
            if (e.Cancelled == true) return;

            image = value;
            Thread.Sleep(0);

            needRedraw = true;
            CalculateDimensions();
            this.Invalidate();

            e = new OControlEventArgs(oi, value, false);
            this.AutoScrollPosition = ptO;
            if (ImageChanged != null)
                ImageChanged(this, e);


        }
    }

    #endregion

    #region Image File Handling

    [Description("Load an image file and redraw the control.")]
    public void LoadFile(String fileName = "")
    {

        if (fileName != "")
        {
            if (File.Exists(fileName) == true) 
            {
                this.fileName = fileName;
            }
        }

        if (this.fileName == "")  
        {
            OpenFileDialog dlg = new System.Windows.Forms.OpenFileDialog();

            if (dlg.ShowDialog() == DialogResult.OK) 
            {
                this.fileName = dlg.FileName;
            }
            else return;
        }

        if (File.Exists(this.fileName) == true)
        {
            image = new System.Drawing.Bitmap(this.fileName);
            needRedraw = true;
            this.Invalidate();
        }
    }

    #endregion
        
    #region Scrolling

        [Browsable(true), Description("Maximum Horizontal Scroll Value in Pixels."), Category("Design")]
        public int MaxScrollX 
        {
            get 
            { 
                if (maxScrollX < this.ClientSize.Width) return this.ClientSize.Width;
                return maxScrollX;
                }
            set 
            { 
                maxScrollX = value;

                needRedraw = true;
                CalculateDimensions();
                this.Invalidate();

                }
        }

        [Browsable(true), Description("Maximum Vertical Scroll Value in Pixels."), Category("Design")]
        public int MaxScrollY 
        {
            get 
            { 
                if (maxScrollY < this.ClientSize.Width) return this.ClientSize.Width;
                return maxScrollY;
                }
            set 
            { 
                maxScrollY = value;

                needRedraw = true;
                CalculateDimensions();
                this.Invalidate();

                }
        }


    /// <summary>
    /// Translates the visible box from the scroll values into virtual coordinates.
    /// </summary>
    /// <param name="subject"> subject for which to return virtual coordinates. Subject must expose Left and Top properties.</param>
    /// <returns>A new System.Drawing.Rectangle containing the virtual coordinates.</returns>
    /// <remarks></remarks>
    public System.Drawing.Rectangle ScrollTranslate(Object subject = null) 
    {
        int x = 0, y = 0, cx = 0, cy = 0;
        dynamic c = subject;

        if (c == null)
        {
            x = this.AutoScrollPosition.X;
            y = this.AutoScrollPosition.Y;

            cx = this.ClientSize.Width;
            cy = this.ClientSize.Height;
        }
        else
        {
            try 
	        {	        
                x = this.AutoScrollPosition.X + c.Left;
                y = this.AutoScrollPosition.Y + c.Top;
                cx = c.Height;
                cy = c.Width;
	        }
	        catch (Exception)
	        {
                return new Rectangle(0, 0, 0, 0);
	        }
        }

        return new System.Drawing.Rectangle(x, y, cx, cy);
    }

    /// <summary>
    /// Calculate the scroll dimensions based upon the current image size and scaling factor.
    /// </summary>
    /// <remarks></remarks>
    public void CalculateDimensions() 
    {
        int x, y, tWidth = 0, tHeight = 0;
        
        this.SuspendLayout();

        // if there is no image, we have no reason to establish scrolling; turn off scrolling.
        if (image == null) 
        {
            this.AutoScrollMinSize = new System.Drawing.Size(0, 0);

            this.HScroll = false;
            this.VScroll = false;

            this.AutoScroll = false;

            ResumeLayout();

            return;
        }

        tWidth = (int) ((float) (image.Width) * scaling);
        tHeight = (int) ((float) (image.Height) * scaling);

        if (tHeight > this.ClientSize.Height) 
        {
            y = tHeight;
            this.VScroll = true;
        }
        else 
        {
            y = 0;
            this.VScroll = false;
        }
        
        if (tWidth > this.ClientSize.Width) 
        {
            x = tWidth;
            this.HScroll = true;
        }
        else
        {
            x = 0;
            this.HScroll = false;
        }

        ResumeLayout();

        this.AutoScroll = false;
        this.AutoScroll = true;

        this.AutoScrollMinSize = new System.Drawing.Size(x, y);

        maxScrollX = x;
        maxScrollY = y;

    }

    /// <summary>
    /// Calculate the absolute distance from the closest boundary of rc to pt.
    /// </summary>
    /// <param name="left">Left coordinate of point.</param>
    /// <param name="top">Top coordinate of point.</param>
    /// <param name="rc">Rectangle on which to base calculations.</param>
    /// <returns>A new size structure representing the boundary distance.</returns>
    /// <remarks></remarks>
    public System.Drawing.Size BoundDistance(int left, int top, Rectangle rc) 
    {
        return BoundDistance(new System.Drawing.Point(left, top), rc);
    }

    /// <summary>
    /// Calculate the absolute distance from the closest boundary of rc to pt.
    /// </summary>
    /// <param name="pt">Point to calculate.</param>
    /// <param name="rc">Rectangle on which to base calculations.</param>
    /// <returns>A new size structure representing the boundary distance.</returns>
    /// <remarks></remarks>
    public System.Drawing.Size BoundDistance(System.Drawing.Point pt, System.Drawing.Rectangle rc)
    {
        int x = 0, y = 0;
        if (pt.X <= rc.Left)
        {
            x = rc.Left - pt.X;
        }
        else if (pt.X > rc.Right)
        {
            x = pt.X - rc.Right;
        }

        if (pt.Y <= rc.Top)
        {
            y = rc.Top - pt.Y;
        }
        else if (pt.Y > rc.Bottom)
        {
            y = pt.Y - rc.Bottom;
        }

        return new System.Drawing.Size(x, y);

    }

    #endregion
                
    #region Scaling


    /// <summary>
    /// Record the last scaling.
    /// </summary>
    /// <param name="value">Scaling to record.</param>
    /// <remarks></remarks>
    protected void SetLastScale(float value) 
    {
        lastScale = value;
    }

    private Rectangle[] cRatios;
    private bool NeedScale = false;
    private bool InScale  = false;

    protected void ScaleControls(bool Record = false)
    {

        if (InScale == true) return;
        InScale = true;

        int i = 0;

        float f, g, h, j;

        int cx, cy;

        if (Record == true)
        {
            cRatios = null;
            cRatios = new Rectangle[this.Controls.Count - 1];

            foreach(Control c in this.Controls) 
            {

                //// get { the virtual position of the control
                cx = c.Left + this.HorizontalScroll.Value;
                cy = c.Top + this.VerticalScroll.Value;

                f = (float) ((cx - imageRect.Left)) / (float) (imageRect.Width);
                g = (float) ((cy - imageRect.Top)) / (float) (imageRect.Height);
                h = (float) (c.Width) / (float) (imageRect.Width);
                j = (float) (c.Height) / (float) (imageRect.Height);

                cRatios[i] = new System.Drawing.Rectangle((int) f, (int) g, (int) h, (int) j);
                i++;

            }

            NeedScale = true;
        }
        else if (NeedScale == true)
        {

            foreach(Control c in this.Controls) 
            {

                f = (float) (imageRect.Left) + (imageRect.Width * cRatios[i].Left);
                g = (float) (imageRect.Top) + (imageRect.Height * cRatios[i].Top);
                h = (imageRect.Width * cRatios[i].Width);
                j = (imageRect.Height * cRatios[i].Height);

                f -= this.HorizontalScroll.Value;
                g -= this.VerticalScroll.Value;

                c.SuspendLayout();

                c.SetBounds((int) f, (int) g, (int) h, (int) j);

                c.ResumeLayout();
                c.Invalidate();


                i++;
            }

        }

        InScale = false;

    }

    #endregion

    #region User Interactions
    
    protected override void OnMouseWheel(System.Windows.Forms.MouseEventArgs e)
    {
        base.OnMouseWheel(e);
    }

    protected override void OnResize(EventArgs e)
    {
        if (this.Parent == null) return;

        this.SuspendLayout();

        if (this.Width > this.Parent.ClientSize.Width)
            this.Width = this.Parent.ClientSize.Width;
        
        if (this.Height > this.Parent.ClientSize.Height)
            this.Height = this.Parent.ClientSize.Height;
        
        needRedraw = true;

        this.ResumeLayout();
        this.Invalidate();

        base.OnResize(e);
    }

    #endregion

    #region Painting and Rendering

    new public void Refresh()
    {
        needRedraw = true;
        base.Refresh();
    }

    public void RefreshPreview(  bool FullRefresh = false)
    {
        if (FullRefresh  == true) 
            needRedraw = true;
       
        InternalPaint();
    }

    protected void FullPaint()
    {
        System.Drawing.Graphics g;

        int x = 0, y = 0;

        int tWidth = 0;
        int tHeight = 0;

        int iWidth = 0;
        int iHeight = 0;

        if (image == null)
        {
            image = new Bitmap(this.ClientRectangle.Width, this.ClientRectangle.Height, PixelFormat.Format32bppArgb);
            g = Graphics.FromImage(image);
            g.Clear(Color.White);
            g.Dispose();
        }

        Rectangle rc;
        SolidBrush brw = new SolidBrush(this.BackColor);

        iWidth = (int) ((float) (Image.Width) * scaling);
        iHeight = (int) ((float) (Image.Height) * scaling);

        tWidth = iWidth;
        tHeight = iHeight;

        if (this.ClientRectangle.Width > tWidth) 
            tWidth = this.ClientRectangle.Width;
        
        if (this.ClientRectangle.Height > tHeight) 
            tHeight = this.ClientRectangle.Height;
        

        rc = new System.Drawing.Rectangle(0, 0, tWidth, tHeight);

        paintPic = new System.Drawing.Bitmap(tWidth, tHeight, PixelFormat.Format32bppArgb);
        g = System.Drawing.Graphics.FromImage(paintPic);

        g.FillRectangle(brw, rc);
        brw.Dispose();

        x = (tWidth / 2) - (iWidth / 2);
        y = (tHeight / 2) - (iHeight / 2);

        imageRect = new System.Drawing.Rectangle(x, y, iWidth, iHeight);

        g.DrawImage(image, imageRect);
        g.Dispose();

    }

    bool Painting = false;

    protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
    {
        if (Painting) return;
        Painting = true;
        // Do some painting
        InternalPaint(e.Graphics);

        // Blt the bitmap.
        e.Graphics.DrawImage(paintPic, new Rectangle(0, 0, this.ClientRectangle.Width, this.ClientRectangle.Height), new Rectangle(HorizontalScroll.Value, VerticalScroll.Value, this.ClientRectangle.Width, this.ClientRectangle.Height), GraphicsUnit.Pixel);
        Painting = false;
    }

    protected void InternalPaint( System.Drawing.Graphics g = null, bool forPreview = false)
    {
        if (needRedraw)
        {
            FullPaint();
            if (CanvasRedrawn != null)
            {
                CanvasRedrawn(this, new OControlEventArgs(false));
            }
            needRedraw = false;
        }
        // Do some other stuff here.


        return;
    }

    #endregion

    #region Constructing

    public CSUCTemplate()
        {
            
            SetStyle(ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint, true);
            UpdateStyles();

            InitializeComponent();
            this.AutoScroll = true;
            this.BackColor = System.Drawing.Color.Transparent;

            // get { the screen dimensions
            Graphics g = this.CreateGraphics();

            // Pixels/Inch
            mdpix = g.DpiX;
            mdpiy = g.DpiY;

            // Pixels/Millimeter
            mdpmx = mdpix / 25.4F;
            mdpmy = mdpiy / 25.4F;

            g.Dispose();

        }

    #endregion

    }

}
